2024_fullman_the_long_string_instrument.py

#

SPDX-FileCopyrightText: 2025 ChloƩ Coppens & Lara Pietkowicz SPDX-FileCopyrightText: 2025 AlICe laboratory https://alicelab.be

SPDX-License-Identifier: GPL-3.0-or-later

import bpy
import bmesh
import pprint
import random
#
def clean():
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete(use_global=False)
    bpy.ops.outliner.orphans_purge()


clean()
#
  1. DATA Generate random script
positions = []
partition = []

for _ in range(43):
    positions.append(random.choice([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]))
    note = []
    for _ in range(random.randint(1, 7)):
        note.append(random.choice([1, 2, 3, 4, 5, 6, 7, 8, 9]))
    partition.append(note)
#

Ellen’s script data partition = [ [1, 5, 2, 7, 3], [4, 5, 8, 1, 9], [1, 5, 2, 7], [3, 1, 2], [3, 1, 2], [9], [1, 5, 2, 5], [8, 1, 9], [8, 1, 9, 1], [6, 3, 2, 7], [8, 1, 9], [8, 1, 9, 4], [1, 5, 4, 9], [1, 4, 6, 3], [1, 5, 4, 9], [1, 4, 6, 3], [1, 5, 4, 9], [2, 5], [6, 3, 2, 5], [6, 3, 2, 5], [1, 5, 2, 5], [9], [1, 4], [1, 4], [6, 3, 2, 5], [1, 5, 4, 9], [2, 7, 3], [7, 1, 1], [6], [6], [3], [3], [2, 7, 3], [6, 3, 2, 5], [6, 3, 2, 5], [5, 5, 3], [6, 3, 2, 5], [6, 3, 2, 5], [5, 5, 3], [6, 3, 2, 5], [6, 3, 2, 5], [8, 1, 9, 1], [4, 5, 8, 1, 9] ] positions = [5, 2, 7, 4, 4, 5, 7, 5, 3, 0, 2, 3, 5, 6, 7, 6, 5, 3, 2, 2, 2, 4, 3, 3, 3, 5, 4, 4, 3, 3, 3, 3, 3, 6, 6, 4, 6, 2, 2, 6, 6, 5, 2]

#

Braille data

braille_values = {
    1: [0],
    2: [0, 2],
    3: [0, 1],
    4: [0, 1, 3],
    5: [0, 3],
    6: [0, 1, 2],
    7: [0, 1, 2, 3],
    8: [0, 2, 3],
    9: [1, 2],
}
#
  1. GENERATE 3D OBJETS 2.1 Generate note’s coordinates
def coordinate_from_position(pos, n):
    y = plane_distance if n % 2 == 0 else -plane_distance
    print("In:", n, y, pos, end=" ")
    return [n, y, pos * z_step]
#

2.2 Generate plane surfaces 2.2.1 Left side

bpy.ops.mesh.primitive_plane_add(location=(30, -36.3, 28))
bpy.context.object.name = "gege-gauche"
bpy.ops.transform.resize(value=(38, 38, 38))
bpy.ops.transform.rotate(value=1.571, orient_axis="X")
bpy.ops.object.modifier_add(type="SOLIDIFY")
bpy.context.object.modifiers["Solidify"].thickness = 0.04
#

2.2.2 Right side

bpy.ops.mesh.primitive_plane_add(location=(30, 37.8, 28))
bpy.context.object.name = "gege-droite"
bpy.ops.transform.resize(value=(38, 38, 38))
bpy.ops.transform.rotate(value=1.571, orient_axis="X")
bpy.ops.object.modifier_add(type="SOLIDIFY")
bpy.context.object.modifiers["Solidify"].thickness = 0.04
#

2.3 Generate Braille’s pattern

def braille_sequence(coord, notes):
    print("with patterns:")
    note_sequence_dot_coordinates = []
    for single_note in notes:
        braille_pattern = braille_values[single_note]
        print("braille:", braille_pattern)
        brailles_dot_coords = braille(coord, braille_pattern)
        note_sequence_dot_coordinates.append(brailles_dot_coords)
        coord[0] += braille_steps
    return note_sequence_dot_coordinates
#

2.4 Braille’s parameters

sphere_radius = 0.35
braille_steps = 2
plane_distance = 76 / 2
braille_dot_offset = 0.5
z_step = 2.3
#

2.5 Generate Braille’s sphere according to pattern

def braille(coord, pattern):
    new_coord = coord.copy()
    dot_coordinates = []
    for dot in pattern:
        if dot == 0:
            new_coord[0] -= braille_dot_offset
            new_coord[2] += braille_dot_offset
#

make a sphere and save coordinate

            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))
#

reset coordinate

            new_coord = coord.copy()

        elif dot == 1:
            new_coord[0] += braille_dot_offset
            new_coord[2] += braille_dot_offset
#

make a sphere and save coordinate

            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))
#

reset coordinate

            new_coord = coord.copy()

        elif dot == 2:
            new_coord[0] += braille_dot_offset
            new_coord[2] -= braille_dot_offset
#

make a sphere and save coordinate

            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))
#

reset coordinate

            new_coord = coord.copy()

        elif dot == 3:
            new_coord[0] = new_coord[0] - braille_dot_offset
            new_coord[2] = new_coord[2] - braille_dot_offset
#

make a sphere and save coordinate

            print("a sphere in", new_coord)
            bpy.ops.mesh.primitive_uv_sphere_add(
                radius=sphere_radius, location=new_coord
            )
            dot_coordinates.append(tuple(new_coord))
#

reset coordinate

            new_coord = coord.copy()

    return dot_coordinates
#

2.6 Generate connections between notes 2.6.1 Generate dots according to the spheres

print("MAKE DOTS")
generated_dots = []
length = 0
for n, pos in enumerate(positions):
    coord = coordinate_from_position(pos * 3, n)
    coord[0] += length
    notes = partition[n]
    print("use", notes, end=" ")
    braille_sequence_dots = braille_sequence(coord, notes)
    length = len(braille_sequence_dots)
    generated_dots.append(braille_sequence_dots)

pprint.pprint(generated_dots)
#

2.6.2 Generate connections between line and dots

print("MAKE LINES")

n = 0
print("A Note:", generated_dots[n])
print("Next Note:", generated_dots[n + 1])
print("A Note's first braille:", generated_dots[n][0])
print("Next Note's first braille:", generated_dots[n + 1][0])
print("Next Note's first braille first dot:", generated_dots[n + 1][0][0])
#

2.7 Generate a line

def make_line(point1, point2, obj_name="custom_line"):
#

Make a line from two points. create a list of vertex coordinates

    vert_coords = [point1, point2]
#

create the mesh data

    mesh_data = bpy.data.meshes.new(f"{obj_name}_data")
#

create the mesh object using the mesh data

    mesh_obj = bpy.data.objects.new(obj_name, mesh_data)
#

add the mesh object into the scene

    bpy.context.scene.collection.objects.link(mesh_obj)
#

create a new bmesh

    bm = bmesh.new()
#

create and add a vertices

    for coord in vert_coords:
        bm.verts.new(coord)
#

connect vertices into edge

    bm.verts.ensure_lookup_table()
    v1, v2 = bm.verts[0], bm.verts[1]
    bm.edges.new((v1, v2))
#

writes the bmesh data into the mesh data

    bm.to_mesh(mesh_data)
#

[Optional] update the mesh data (helps with redrawing the mesh in the viewport)

    mesh_data.update()
#

clean up/free memory that was allocated for the bmesh

    bm.free()
#

return object for later use

    return bpy.data.objects[obj_name]
#
def get_dot_coordinates(note):
    dot_coord = []
    for braille in note:
        for dot in braille:
            dot_coord.append(dot)
    return dot_coord
#
def connect_brailles(note1, note2):
    side1 = get_dot_coordinates(note1)
    side2 = get_dot_coordinates(note2)

    random.shuffle(side1)
    random.shuffle(side2)

    if len(side1) > len(side2):
        reserve = side2.copy()

        for n, dot1 in enumerate(side1):
            if n < len(side2):
                make_line(dot1, side2.pop())
            else:
                make_line(dot1, random.choice(reserve))

    else:
        for dot1 in side1:
            reserve = side1.copy()
            for n, dot2 in enumerate(side2):
                if n < len(side1):
                    make_line(dot2, side1.pop())
                else:
                    make_line(dot2, random.choice(reserve))
#

2.8 Convert lines in mesh

note_number = len(generated_dots)
for n in range(note_number):
    if n + 1 < note_number:
        connect_brailles(generated_dots[n], generated_dots[n + 1])
#

2.9 Convert mesh in tubes

for obj in bpy.data.objects:
    if "line" in obj.name:
        obj.select_set(True)
        bpy.context.view_layer.objects.active = obj
        bpy.ops.object.convert(target="CURVE")
        bpy.context.object.data.bevel_depth = 0.2
        bpy.context.object.data.use_fill_caps = True